﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ESRI.ArcGIS.Carto;
using ESRI.ArcGIS.DataSourcesGDB;
using ESRI.ArcGIS.DataSourcesRaster;
using ESRI.ArcGIS.Geoprocessor;
using ESRI.ArcGIS.Geoprocessing;
using ESRI.ArcGIS.Geodatabase;
using ESRI.ArcGIS.esriSystem;

namespace Bes.Swsp.ToolBox
{
  public class GeoprocessingUtility
  {
    private static bool? _userTempLayers;
    private static bool? _userAddOutputs;
    private static bool? _userOverwrite;
    private static object _scratchWorkspace;
    private static object _workspace;

    public static string ScratchWorkspace
    {
      get
      {
        Geoprocessor gp = new Geoprocessor();
        return Convert.ToString(gp.GetEnvironmentValue("scratchWorkspace"));
      }
    }

    public static Geoprocessor GetGeoprocessor(bool temporaryMapLayers, bool addOutputsToMap, bool overwriteOutput, IRaster gridReference = null)
    {
      Geoprocessor gp = new Geoprocessor();
      if (!_userTempLayers.HasValue)
        _userTempLayers = gp.TemporaryMapLayers;
      if (!_userAddOutputs.HasValue)
        _userAddOutputs = gp.AddOutputsToMap;
      if (!_userOverwrite.HasValue)
        _userOverwrite = gp.OverwriteOutput;

      gp.TemporaryMapLayers = temporaryMapLayers;

      //TODO: Python Toolbox accumulate tool crashes here due to not finding Esri.ArcGIS.Desktop.Addins
      gp.AddOutputsToMap = ArcMap.Document == null ? false : addOutputsToMap;
      gp.OverwriteOutput = overwriteOutput;

      if (gridReference != null)
      {
        IRasterDataset rds = ((IRasterAnalysisProps)gridReference).RasterDataset;
        IDataset ds = (IDataset)rds;
        string path = System.IO.Path.Combine(ds.Workspace.PathName, ds.Name);
        gp.SetEnvironmentValue("snapRaster", path);

        string extent = string.Format("{0} {1} {2} {3}", ((IGeoDataset2)rds).Extent.XMin, ((IGeoDataset2)rds).Extent.YMin, ((IGeoDataset2)rds).Extent.XMax, ((IGeoDataset2)rds).Extent.YMax);
        gp.SetEnvironmentValue("extent", extent);
      }

      if (_workspace == null)
        _workspace = gp.GetEnvironmentValue("workspace");
      if (_scratchWorkspace == null)
        _scratchWorkspace = gp.GetEnvironmentValue("scratchWorkspace");

      if (gp.GetEnvironmentValue("workspace") == null)
        gp.SetEnvironmentValue("workspace", _GetDefaultWorkspace());
      if(gp.GetEnvironmentValue("scratchWorkspace") == null)
        gp.SetEnvironmentValue("scratchWorkspace", _GetDefaultWorkspace());

      return gp;
    }

    private static string _GetDefaultWorkspace()
    {
      string documents = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
      string tempDir = System.IO.Path.Combine(documents, "temp");
      string workspacePath = System.IO.Path.Combine(tempDir, "Scratch.gdb");

      IWorkspaceFactory workspaceFactory = new FileGDBWorkspaceFactory();
      if (!workspaceFactory.IsWorkspace(workspacePath))
      {
        IWorkspaceName defaultWorkspaceName = workspaceFactory.Create(tempDir, "Scratch", null, 0);
        return defaultWorkspaceName.PathName;
      }
      else
      {
        return workspacePath;
      }
    }

    public static void ResetGeoprocessor()
    {
      Geoprocessor gp = new Geoprocessor();
      if (_userTempLayers.HasValue)
        gp.TemporaryMapLayers = _userTempLayers.Value;
      if (_userAddOutputs.HasValue)
        gp.AddOutputsToMap = _userAddOutputs.Value;
      if (_userOverwrite.HasValue)
        gp.OverwriteOutput = _userOverwrite.Value;
      if (_workspace != null)
        gp.SetEnvironmentValue("workspace", _workspace);
      if (_scratchWorkspace != null)
        gp.SetEnvironmentValue("scratchWorkspace", _scratchWorkspace);

      _userTempLayers = null;
      _userAddOutputs = null;
      _userOverwrite = null;
      _workspace = null;
      _scratchWorkspace = null;
    }

    public static object RunGpTool(Geoprocessor geoprocessor, IGPProcess process, bool async = true)
    {
      return RunGpTool(geoprocessor, process, _ProgressSink, async);
    }

    public static object RunGpTool(Geoprocessor geoprocessor, IGPProcess process, Action<ProgressEventArgs, bool> progressEvent, bool async = true)
    {
      if (async && ArcMap.Application != null)
        return _RunGpToolAsync(geoprocessor, process, progressEvent);
      else
        return _RunGpToolSync(geoprocessor, process, progressEvent);
    }

    public static string GetFeatureClassPath(IFeatureClass featureClass)
    {
      if (featureClass != null)
      {
        string path = ((IDataset)featureClass).Workspace.PathName;
        if (featureClass.FeatureDataset != null)
        {
          path += "\\" + featureClass.FeatureDataset.Name;
        }
        path += "\\" + ((IDataset)featureClass).Name;
        return path;
      }
      else
      {
        return string.Empty;
      }
    }

    public static string GetRasterPath(IRaster raster)
    {
      if (raster == null)
      {
        return string.Empty;
      }

      IRasterDataset rasterDataset = ((IRasterAnalysisProps)raster).RasterDataset;
      if (rasterDataset != null)
      {
        string path = ((IDataset)rasterDataset).Workspace.PathName;
        path += "\\" + ((IDataset)rasterDataset).Name;
        return path;
      }
      else
      {
        return string.Empty;
      }
    }

    private static object _RunGpToolAsync(Geoprocessor geoprocessor, IGPProcess process, Action<ProgressEventArgs, bool> progressEvent)
    {
      // Execute the tool   
      IGeoProcessorResult result;
      try
      {
        bool tryAgain = false;
        int retryCount = 0;
        int maxRetries = 1;
        ProgressEventArgs progressEventArgs = null;
         
        if(ArcMap.Document != null)
          ((IMapCache)ArcMap.Document.FocusMap).EmptyCache();

        do
        {
          result = geoprocessor.ExecuteAsync(process);
          string message = "Process " + process.ToolName + " submitted to Geoprocessor queue. (Asynchronous)";
          if (progressEvent != null)
          {
            progressEventArgs = new ProgressEventArgs(0, 1, message, true);
            progressEvent(progressEventArgs, true);
          }

          bool waiting = true;
          while (waiting)
          {
            System.Threading.Thread.Sleep(200);

            if (result.Status == esriJobStatus.esriJobSucceeded)
            {
              waiting = false;
              tryAgain = false;
              message = result.GetMessages(1);
              message += result.GetMessages(0);
            }
            else if (result.Status >= esriJobStatus.esriJobFailed)
            {
              waiting = false;
              tryAgain = true;
              message = result.GetMessages(2);

              retryCount++;
              if (retryCount > maxRetries)
              {
                Exception innerException = new Exception(result.GetMessages(2));
                throw new GeoprocessingException("Geoprocessing tool " + process.ToolName + " failed after " + retryCount + " attempts.", innerException);
              }
            }
            else if (progressEvent != null && progressEventArgs != null && progressEventArgs.Cancel)
            {
              result.Cancel();
              throw new UserCanceledException();
            }
            else
            {
              message = "Process status for " + process.ToolName + ": " + result.Status.ToString();
            }

            if (progressEvent != null)
            {
              progressEventArgs = new ProgressEventArgs(0, 1, message, true);
              progressEvent(progressEventArgs, false);
            }
          }

        }
        while (tryAgain);

        if (ArcMap.Document != null)
          ((IMapCache)ArcMap.Document.FocusMap).EmptyCache();

        return result.ReturnValue;
      }
      catch (Exception ex)
      {
        Logger.Log(string.Format("GeoprocessingUtility._RunGpToolAsync: {0}: {1}", ex.GetType().FullName, ex.Message));
        throw;
      }
    }

    private static object _RunGpToolSync(Geoprocessor geoprocessor, IGPProcess process, Action<ProgressEventArgs, bool> progressEvent)
    {
      // Execute the tool   
      object result = null;
      try
      {
        bool tryAgain = false;
        int retryCount = 0;
        int maxRetries = 1;

        do
        {
          retryCount++;
          try
          {
            if (progressEvent != null)
              progressEvent(new ProgressEventArgs(0, 1, "Executing " + process.ToolName + " Geoprocessor tool. (Synchronous)"), true);

            geoprocessor.ProgressChanged += geoprocessor_ProgressChanged;
            geoprocessor.MessagesCreated += geoprocessor_MessagesCreated;

            IGeoProcessorResult2 gpResult = geoprocessor.Execute(process, null) as IGeoProcessorResult2;
            
            result = gpResult.ReturnValue;

            tryAgain = false;
          }
          catch(Exception ex)
          {
            Logger.Log(string.Format("GeoprocessingUtility._RunGpToolSync: {0}: {1}", ex.GetType().FullName, ex.Message));
            if (retryCount <= maxRetries)
            {
              tryAgain = true;
            }
            else
            {
              throw new GeoprocessingException("Geoprocessing tool " + process.ToolName + " failed after " + retryCount + " attempts.", ex);
            }
          }
        }
        while (tryAgain);

        return result;
      }
      catch (Exception ex)
      {
        Logger.Log(string.Format("GeoprocessingUtility._RunGpToolSync: {0}: {1}", ex.GetType().FullName, ex.Message));
        throw;
      }
    }

    static void geoprocessor_MessagesCreated(object sender, MessagesCreatedEventArgs e)
    {
      for (int i =0; i <e.GPMessages.Count; i++)
        Logger.Log(e.GPMessages.GetMessage(i).Description);

      e.GPMessages.Clear();
    }

    static void geoprocessor_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
      Logger.Log(e.Message);
    }



    private static void _ProgressSink(ProgressEventArgs eventArgs, bool log = false)
    {
      // Ensures that ArcMap allows geoprocessor to start when a progress event handler is not specified
      System.Windows.Forms.Application.DoEvents();

      if (log)
      {
        Logger.Log(eventArgs.Message);
      }
    }
  }
}
